﻿namespace Contoso.ActiveDirectory {

    #region Usings
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.DirectoryServices;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Net.Sockets;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    #endregion

    /// <summary>
    /// Objects that are DNS Zones.
    /// Zones may be scoped at different levels (Domain, Forest, Application Partition)
    /// For the default zone scope, base DN for queries: CN=MicrosoftDNS,DC=DomainDNSZones,DC=domain,DC=com
    /// For domain scope "To all DNS servers running on domain controllers in this forest", the base DN location is: CN=MicrosoftDNS,DC=ForestDNSZones,DC=domain,DC=com
    /// For domain scope "To all DNS servers running on domain controllers in this domain", the base DN location is: CN=MicrosoftDNS,DC=DomainDNSZones,DC=domain,DC=com
    /// For domain scope "To all domain controllers in this domain (for Windows 2000 compatibility): the base DN location is: CN=MicrosoftDNS,CN=System,DC=domain,DC=com
    /// </summary>
    [Serializable]
    public class DNSZone : ADBaseObjectProperties {

        #region Members
        public static readonly new string[] AttributesToGet;

        /// <summary>
        /// Specifies if zone aging is configured
        /// </summary>
        public bool Aging { get; private set; }

        /// <summary>
        /// The datetime that aging was enabled
        /// </summary>
        public DateTime? AgingEnabledDate { get; private set; }

        /// <summary>
        /// A list of DNS servers that may autocreate a DNS delegation
        /// </summary>
        public IReadOnlyList<IPAddress> AllowNSRecordsAutoCreation { get; private set; }

        /// <summary>
        /// 
        /// </summary>
        public string DeletedFromHostname { get; private set; }

        /// <summary>
        /// The DNS zone dynamic update configuration
        /// </summary>
        public DNSZoneDynamicUpdate DynamicUpdate { get; private set; }

        /// <summary>
        /// The list of DNS servers that will perform zone transfers
        /// </summary>
        public IReadOnlyList<IPAddress> MasterServers { get; private set; }

        /// <summary>
        /// The zone scavenging no-refresh interval.
        /// Time during which the server does not accept updates to the timestamp for the record. (The server still accepts updates for the address).
        /// This value is the interval between the last time a record was refreshed and the earliest moment it can be refreshed again. 
        /// </summary>
        public TimeSpan NoRefreshInterval { get; private set; }

        /// <summary>
        /// The zone scavenging refresh interval.
        /// he refresh interval comes after the no-refresh interval. 
        /// At the beginning of the refresh interval, the server accepts updates to the timestamp.
        /// After the refresh interval expires, the DNS server can scavenge records that have not been refreshed during or after the refresh interval. 
        /// </summary>
        public TimeSpan RefreshInterval { get; private set;  }

        /// <summary>
        /// The list of servers that will perform scavenging of the zone
        /// </summary>
        public IReadOnlyList<IPAddress> ScavengeServers { get; private set; }

        /// <summary>
        /// The time at which the zone's AllowUpdate property (section 3.1.1.2.1) was last changed from any value to ZONE_UPDATE_SECURE 
        /// Changes of the zone's AllowUpdate property to any other value MUST NOT cause a change to Time Zone Secured.
        /// The value is expressed as the number of seconds since 12:00 A.M. January 1, 1601 Coordinated Universal Time (UTC) 
        /// in unsigned 64-bit integer format. The default value of this property MUST be zero.
        /// If this zone is stored in the local directory server, then this value is initialized from and written to the "whenChanged" 
        /// attribute of the "dnsZone".
        /// </summary>
        public DateTime? SecureTime { get; private set; }

        /// <summary>
        /// The DNS Zone Node DBFlags
        /// </summary>
        public DNSRPCNodeFlags ZoneNodeDBFlags { get; private set; }

        /// <summary>
        /// The type of DNS Zone
        /// </summary>
        public DNSZoneType ZoneType { get; private set; }

        /// <summary>
        /// The DNSProperty attribute of the zone.
        /// Used to store binary settings (properties) on DNS zone objects.
        /// </summary>
        /// <example>
        /// [DataLength] Byte: 0 (one byte) An unsigned binary integer containing the length, in bytes, of the Data field.
        /// [NameLength] Byte: 1 (one byte) Not Used. The value MUST be ignored and assumed to be 0x00000001.
        /// [Flag] Byte 2 (one byte) This field is reserved for future use. The value MUST be 0x00000000.
        /// [Version] Byte: 3 (one byte) The version number associated with the property attribute. The value MUST be 0x00000001.
        /// [Id] Byte: 4 (one byte) The property attribute's type. See DNSZonePropertyId enum.
        /// [Data] Byte: 5 - ... (variable length).  The data associated with the DNSZonePropertyId.
        /// [Name] the last byte. Not used, and MUST be ignored.
        /// </example>
        public IReadOnlyList<byte[]> DNSProperty { get; private set; }

        #region ObjectCategory/ObjectClass identifiers
        public static readonly string ObjectCategoryShortName = "dnsZone";
        public static readonly string ObjectClassShortName = "dnsZone";
        #endregion
        #endregion

        #region Constructor
        static DNSZone() {
            var attributesToGet = new List<string>();

            attributesToGet.AddRange(new string[]
            {
                "dnsProperty"
            });

            attributesToGet.AddRange(ADBaseObjectProperties.AttributesToGet);

            AttributesToGet = attributesToGet.Distinct().ToArray();
            attributesToGet = null;
        }

        public DNSZone() {
            this.Initialize();
        }

        public DNSZone(DirectoryEntry directoryEntry, bool destroyInputObject = true)
            : base(directoryEntry, destroyInputObject) {
            if (this.GetType().Name == typeof(DNSZone).Name) {
                this.Attributes = null;
            }
        }

        public DNSZone(SearchResult result, bool destroyInputObject = true)
            : base(result, destroyInputObject) {
            if (this.GetType().Name == typeof(DNSZone).Name) {
                this.Attributes = null;
            }
        }
        #endregion

        #region Methods
        protected override void Initialize() {
            base.Initialize();

            this.DeletedFromHostname = string.Empty;

            this.AllowNSRecordsAutoCreation = new List<IPAddress>();
            this.DNSProperty = new List<byte[]>();
            this.MasterServers = new List<IPAddress>();
            this.ScavengeServers = new List<IPAddress>();
        }

        protected override void ProcessResults() {

            try {
                base.ProcessResults();

                if (this.Attributes.ContainsKey("dnsProperty")) {
                    var dnsProperty = new List<byte[]>();
                    for (int index = 0; index < this.Attributes["dnsProperty"].Length; index++) {
                        if (this.Attributes["dnsProperty"][index] as byte[] != null) {
                            var bytes = (byte[])this.Attributes["dnsProperty"][index];
                            dnsProperty.Add(bytes);
                        }
                    }
                    this.DNSProperty = dnsProperty;
                }

                foreach (var dnsProperty in this.DNSProperty) {
                    var memoryStream = new MemoryStream(dnsProperty);
                    using (var propertyReader = new EndianBinaryReader(memoryStream)) {
                        this.ReadDNSProperty(propertyReader);
                    }
                }

                this.IsValid = !string.IsNullOrWhiteSpace(this.DN) &&
                    this.ObjectClass.Any(x => x.Equals(ObjectCategoryShortName, StringComparison.OrdinalIgnoreCase));

            }
            catch (Exception e) {
                Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} Error: {3}", DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), e.VerboseExceptionString()));
                throw;
            }

        }
        
        /// <summary>
        /// [DataLength] Byte: 0 (one byte) An unsigned binary integer containing the length, in bytes, of the Data field.
        /// [NameLength] Byte: 1 (one byte) Not Used. The value MUST be ignored and assumed to be 0x00000001.
        /// [Flag] Byte 2 (one byte) This field is reserved for future use. The value MUST be 0x00000000.
        /// [Version] Byte: 3 (one byte) The version number associated with the property attribute. The value MUST be 0x00000001.
        /// [Id] Byte: 4 (one byte) The property attribute's type. See DNSZonePropertyId enum.
        /// [Data] Byte: 5 - ... (variable length).  The data associated with the DNSZonePropertyId.
        /// [Name] the last byte. Not used, and MUST be ignored.
        /// </summary>
        /// <param name="propertyReader"></param>
        private void ReadDNSProperty(EndianBinaryReader propertyReader) {
            uint dataLength = propertyReader.ReadUInt32();
            uint nameLength = propertyReader.ReadUInt32();
            // Must be 0
            uint flag = propertyReader.ReadUInt32();
            // Must be 1
            uint version = propertyReader.ReadUInt32();
            DNSZonePropertyId propertyID = (DNSZonePropertyId)propertyReader.ReadUInt32();

            uint numberOfServers;
            switch (propertyID) {
                case DNSZonePropertyId.DSPROPERTY_ZONE_AGING_ENABLED_TIME:
                    uint agingEnabledDateHours = propertyReader.ReadUInt32();
                    if (agingEnabledDateHours > 0) {
                        this.AgingEnabledDate = WindowsEpochStart.AddHours(agingEnabledDateHours);
                    }
                    break;
                case DNSZonePropertyId.DSPROPERTY_ZONE_AGING_STATE:
                    if ((uint)propertyReader.ReadUInt32() == 1) {
                        this.Aging = true;
                    }
                    break;
                case DNSZonePropertyId.DSPROPERTY_ZONE_ALLOW_UPDATE:
                    this.DynamicUpdate = (DNSZoneDynamicUpdate)propertyReader.ReadByte();
                    break;
                case DNSZonePropertyId.DSPROPERTY_ZONE_AUTO_NS_SERVERS_DA:
                //break;                
                case DNSZonePropertyId.DSPROPERTY_ZONE_AUTO_NS_SERVERS:
                    if (dataLength >= 4) {
                        var allowNSRecordsAutoCreation = new List<IPAddress>();
                        numberOfServers = propertyReader.ReadUInt32();
                        for (int index = 0; index < numberOfServers; index++) {
                            allowNSRecordsAutoCreation.Add(propertyReader.ReadIPAddress());
                        }
                        this.AllowNSRecordsAutoCreation = allowNSRecordsAutoCreation;
                    }
                    break;
                case DNSZonePropertyId.DSPROPERTY_ZONE_DCPROMO_CONVERT:
                    break;
                case DNSZonePropertyId.DSPROPERTY_ZONE_DELETED_FROM_HOSTNAME:
                    var unicodeEncoding = new UnicodeEncoding();
                    this.DeletedFromHostname = unicodeEncoding.GetString(propertyReader.ReadBytes((Int32)dataLength));
                    break;
                case DNSZonePropertyId.DSPROPERTY_ZONE_MASTER_SERVERS:
                    //break;
                case DNSZonePropertyId.DSPROPERTY_ZONE_MASTER_SERVERS_DA:
                    uint maxCount = propertyReader.ReadUInt32();
                    uint addrCount = propertyReader.ReadUInt32();
                    byte[] stuff1 = propertyReader.ReadBytes(24);

                    var masterServers = new List<IPAddress>();
                    for (int index = 0; index < addrCount; index++) {
                        var addressFamily = (AddressFamily)propertyReader.ReadUInt16();
                        ushort port = propertyReader.ReadUInt16(isBigEndian: true);

                        IPAddress ipv4Address = propertyReader.ReadIPAddress();
                        IPAddress ipv6Address = propertyReader.ReadIPv6Address();

                        if (addressFamily == AddressFamily.InterNetwork) {
                            masterServers.Add(ipv4Address);
                        }
                        else if (addressFamily == AddressFamily.InterNetworkV6) {
                            masterServers.Add(ipv6Address);
                        }

                        byte[] stuff2 = propertyReader.ReadBytes(8);

                        uint SALen = propertyReader.ReadUInt32();

                        byte[] stuff3 = propertyReader.ReadBytes(28);
                    }
                    this.MasterServers = masterServers;
                    break;
                case DNSZonePropertyId.DSPROPERTY_ZONE_NODE_DBFLAGS:
                    this.ZoneNodeDBFlags = (DNSRPCNodeFlags)propertyReader.ReadUInt32();
                    break;
                case DNSZonePropertyId.DSPROPERTY_ZONE_NOREFRESH_INTERVAL:
                    this.NoRefreshInterval = new TimeSpan((int)propertyReader.ReadUInt32(), 0, 0);
                    break;
                case DNSZonePropertyId.DSPROPERTY_ZONE_REFRESH_INTERVAL:
                    this.RefreshInterval = new TimeSpan((int)propertyReader.ReadUInt32(), 0, 0);
                    break;
                case DNSZonePropertyId.DSPROPERTY_ZONE_SCAVENGING_SERVERS_DA:
                    //Console.WriteLine(String.Format("Property: {0} with DataLength: {1}, NameLength: {2} and Value: {3}", PropertyID, DataLength, NameLength, 0));
                    //break;                
                case DNSZonePropertyId.DSPROPERTY_ZONE_SCAVENGING_SERVERS:
                    // minimum four bytes expected
                    if (dataLength >= 4) {
                        var scavengeServers = new List<IPAddress>();
                        numberOfServers = propertyReader.ReadUInt32();
                        for (int index = 0; index < numberOfServers; index++) {
                            scavengeServers.Add(propertyReader.ReadIPAddress());
                        }
                        this.ScavengeServers = scavengeServers;
                    }
                    break;
                case DNSZonePropertyId.DSPROPERTY_ZONE_SECURE_TIME:
                    ulong securetimeSeconds = propertyReader.ReadUInt64();
                    if (securetimeSeconds > 0) {
                        this.SecureTime = WindowsEpochStart.AddSeconds(securetimeSeconds);
                    }
                    break;
                case DNSZonePropertyId.DSPROPERTY_ZONE_TYPE:
                    this.ZoneType = (DNSZoneType)propertyReader.ReadUInt32();
                    break;
            }
        }
        #endregion
    }
}
